<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>近视随访打卡工具</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
    <style>
        body { font-family: 'Inter', 'Noto Sans SC', sans-serif; -webkit-tap-highlight-color: transparent; }
        .calendar-grid { grid-template-columns: repeat(7, minmax(0, 1fr)); }
        .day-cell { transition: all 0.2s ease-in-out; }
        .day-cell.today { box-shadow: 0 0 0 2px #3b82f6; }
        .day-cell.checked { background-color: #10b981; color: white; transform: scale(1.05); }
        .day-cell.missed { background-color: #f3f4f6; color: #9ca3af; }
        .day-cell.future { background-color: #f9fafb; color: #d1d5db; pointer-events: none; }
        .day-cell.can-makeup { cursor: pointer; background-color: #fee2e2; color: #ef4444; }
        .day-cell.can-makeup:hover { background-color: #fecaca; }
        #loading-overlay { z-index: 50; }
        .view { display: none; }
        .view.active { display: block; }
        .form-input {
            width: 100%;
            padding: 0.75rem;
            border: 1px solid #d1d5db;
            border-radius: 0.5rem;
            transition: border-color 0.2s, box-shadow 0.2s;
        }
        .form-input:focus {
            outline: none;
            border-color: #3b82f6;
            box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4);
        }
    </style>
</head>
<body class="bg-gray-50 flex flex-col items-center min-h-screen p-4 justify-center">

    <div id="loading-overlay" class="fixed inset-0 bg-white bg-opacity-75 backdrop-blur-sm flex items-center justify-center">
        <div class="flex flex-col items-center"><svg class="animate-spin h-8 w-8 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg><p class="mt-4 text-gray-600 font-medium">正在连接服务器...</p></div>
    </div>

    <div class="w-full max-w-md mx-auto">
        <!-- Login/Register View -->
        <div id="login-view" class="view active bg-white rounded-2xl shadow-lg p-6">
            <h2 class="text-2xl font-bold text-gray-800 mb-6 text-center">欢迎使用随访工具</h2>
            <div class="space-y-4">
                 <div>
                    <label for="assigned-id" class="block text-sm font-medium text-gray-700 mb-1">您的专属ID</label>
                    <input type="text" id="assigned-id" class="form-input" placeholder="请输入研究人员分配给您的ID">
                </div>
                <div>
                    <label for="login-name" class="block text-sm font-medium text-gray-700 mb-1">姓名</label>
                    <input type="text" id="login-name" class="form-input" placeholder="请输入您的姓名">
                </div>
                <div id="registration-fields" class="hidden space-y-4">
                     <div>
                        <label for="patient-age" class="block text-sm font-medium text-gray-700 mb-1">年龄</label>
                        <input type="number" id="patient-age" class="form-input" placeholder="请输入您的年龄">
                    </div>
                    <div>
                        <label for="patient-sex" class="block text-sm font-medium text-gray-700 mb-1">性别</label>
                        <select id="patient-sex" class="form-input">
                            <option value="男">男</option>
                            <option value="女">女</option>
                        </select>
                    </div>
                </div>
                <p id="login-error" class="text-red-500 text-sm hidden"></p>
                <button id="login-btn" class="w-full bg-blue-500 text-white font-bold py-3 px-4 rounded-lg hover:bg-blue-600 transition-colors">登录 / 注册</button>
            </div>
        </div>

        <!-- Patient View -->
        <div id="patient-view" class="view">
            <div class="bg-white rounded-2xl shadow-lg p-6">
                <div class="flex justify-between items-center mb-4">
                    <p class="text-lg font-semibold text-gray-800">欢迎, <span id="welcome-name"></span>!</p>
                    <button id="logout-btn-patient" class="text-sm text-red-500 hover:underline">退出登录</button>
                </div>
                <div class="flex items-center justify-between mb-6"><button id="prev-month" class="p-2 rounded-full hover:bg-gray-100"><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg></button><h2 id="current-month-year" class="text-xl font-bold text-gray-800"></h2><button id="next-month" class="p-2 rounded-full hover:bg-gray-100"><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg></button></div>
                <div class="grid grid-cols-7 gap-2 text-center text-sm font-medium text-gray-500 mb-2"><div>日</div><div>一</div><div>二</div><div>三</div><div>四</div><div>五</div><div>六</div></div>
                <div id="calendar-body" class="grid calendar-grid gap-2"></div>
                <div class="mt-6 border-t pt-4 space-y-3">
                    <div class="flex justify-between items-center text-gray-700"><span class="font-medium">总打卡天数:</span><span id="total-checked-days" class="font-bold text-lg text-green-500">0</span></div>
                    <div class="flex justify-between items-center text-gray-700"><span class="font-medium">剩余补打卡次数:</span><span id="makeup-chances" class="font-bold text-lg text-red-500">3</span></div>
                </div>
            </div>
        </div>

        <!-- Researcher View -->
        <div id="researcher-view" class="view">
            <div class="bg-white rounded-2xl shadow-lg p-6">
                <div class="flex justify-between items-center mb-4">
                    <h2 class="text-2xl font-bold text-gray-800">研究人员仪表盘</h2>
                    <button id="logout-btn-researcher" class="text-sm text-red-500 hover:underline">退出登录</button>
                </div>
                <button id="export-csv-btn" class="w-full mb-4 bg-blue-500 text-white font-bold py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors">导出所有数据 (CSV)</button>
                <div class="overflow-x-auto">
                    <table class="min-w-full divide-y divide-gray-200">
                        <thead class="bg-gray-50"><tr><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th><th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase">姓名</th><th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase">总打卡</th><th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase">补卡次数</th></tr></thead>
                        <tbody id="participants-table-body" class="bg-white divide-y divide-gray-200"></tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>

    <div id="makeup-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden"><div class="bg-white rounded-lg shadow-xl p-6 w-11/12 max-w-sm text-center"><h3 class="text-lg font-semibold mb-2">确认补打卡</h3><p class="text-gray-600 mb-6">您确定要使用 1 次补打卡机会为 <strong id="makeup-date-text"></strong> 补卡吗？</p><div class="flex justify-center gap-4"><button id="confirm-makeup" class="bg-blue-500 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-600">确定</button><button id="cancel-makeup" class="bg-gray-200 text-gray-800 px-6 py-2 rounded-lg font-medium hover:bg-gray-300">取消</button></div></div></div>
    
    <script type="module">
        import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
        import { getAuth, signInAnonymously, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
        import { getFirestore, doc, setDoc, onSnapshot, collection, query, getDoc, getDocs, addDoc, where } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";

        // IMPORTANT: Set your admin ID here
        const ADMIN_ID = "ADMIN"; 

        const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : { apiKey: "MOCK_API_KEY", authDomain: "MOCK_AUTH_DOMAIN", projectId: "MOCK_PROJECT_ID" };
        const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';

        const app = initializeApp(firebaseConfig);
        const auth = getAuth(app);
        const db = getFirestore(app);

        document.addEventListener('DOMContentLoaded', () => {
            const loadingOverlay = document.getElementById('loading-overlay');
            // Views
            const loginView = document.getElementById('login-view'), patientView = document.getElementById('patient-view'), researcherView = document.getElementById('researcher-view');
            // Login View Elements
            const assignedIdInput = document.getElementById('assigned-id'), loginNameInput = document.getElementById('login-name'), registrationFields = document.getElementById('registration-fields'), patientAgeInput = document.getElementById('patient-age'), patientSexInput = document.getElementById('patient-sex'), loginBtn = document.getElementById('login-btn'), loginError = document.getElementById('login-error');
            // Patient View Elements
            const welcomeName = document.getElementById('welcome-name'), calendarBody = document.getElementById('calendar-body'), currentMonthYear = document.getElementById('current-month-year'), prevMonthBtn = document.getElementById('prev-month'), nextMonthBtn = document.getElementById('next-month'), totalCheckedDaysEl = document.getElementById('total-checked-days'), makeupChancesEl = document.getElementById('makeup-chances'), logoutBtnPatient = document.getElementById('logout-btn-patient');
            // Modal Elements
            const makeupModal = document.getElementById('makeup-modal'), confirmMakeupBtn = document.getElementById('confirm-makeup'), cancelMakeupBtn = document.getElementById('cancel-makeup'), makeupDateText = document.getElementById('makeup-date-text');
            // Researcher View Elements
            const participantsTableBody = document.getElementById('participants-table-body'), exportCsvBtn = document.getElementById('export-csv-btn'), logoutBtnResearcher = document.getElementById('logout-btn-researcher');

            let state = { currentDate: new Date(), today: new Date(), checkIns: {}, makeupChances: 3, dateToMakeup: null, firestoreDocId: null, unsubscribePatient: null, unsubscribeResearcher: null };
            let isAuthReady = false;

            onAuthStateChanged(auth, async (user) => {
                if (user) {
                    isAuthReady = true;
                    const savedDocId = localStorage.getItem('myopiaTrackerDocId');
                    if (savedDocId) {
                        await attemptAutoLogin(savedDocId);
                    } else {
                        loadingOverlay.classList.add('hidden');
                    }
                } else {
                    try {
                        await signInAnonymously(auth);
                    } catch (error) {
                        console.error("Firebase Auth Error:", error);
                        loginError.textContent = "认证失败，请刷新页面。";
                        loginError.classList.remove('hidden');
                        loadingOverlay.classList.add('hidden');
                    }
                }
            });
            
            const showView = (viewToShow) => {
                [loginView, patientView, researcherView].forEach(v => v.classList.remove('active'));
                viewToShow.classList.add('active');
            };

            const logout = () => {
                localStorage.removeItem('myopiaTrackerDocId');
                state.firestoreDocId = null;
                if (state.unsubscribePatient) state.unsubscribePatient();
                if (state.unsubscribeResearcher) state.unsubscribeResearcher();
                assignedIdInput.value = '';
                loginNameInput.value = '';
                patientAgeInput.value = '';
                registrationFields.classList.add('hidden');
                loginBtn.textContent = "登录 / 注册";
                loginError.classList.add('hidden');
                showView(loginView);
            };
            
            logoutBtnPatient.addEventListener('click', logout);
            logoutBtnResearcher.addEventListener('click', logout);

            // --- Firestore Refs ---
            const getPrivateDataRef = (docId) => doc(db, `artifacts/${appId}/users/${docId}/checkInData/user_data`);
            const getPublicSummaryRef = (docId) => doc(db, `artifacts/${appId}/public/data/participants/${docId}`);
            const getParticipantsCollectionRef = () => collection(db, `artifacts/${appId}/public/data/participants`);

            const attemptAutoLogin = async (docId) => {
                loadingOverlay.classList.remove('hidden');
                loadingOverlay.querySelector('p').textContent = '正在自动登录...';

                // Admin check is separate and doesn't rely on docId
                const savedAssignedId = localStorage.getItem('myopiaTrackerAssignedId_temp'); // Temp check for admin
                 if (docId.toUpperCase() === ADMIN_ID) {
                    setupResearcherListener();
                    showView(researcherView);
                    loadingOverlay.classList.add('hidden');
                    return;
                }

                const publicRef = getPublicSummaryRef(docId);
                const docSnap = await getDoc(publicRef);

                if (docSnap.exists()) {
                    state.firestoreDocId = docId;
                    setupPatientListener(docId);
                    welcomeName.textContent = docSnap.data().name || '用户';
                    showView(patientView);
                } else {
                    logout(); // Clear invalid stored ID
                }
                loadingOverlay.classList.add('hidden');
            };

            const loginAsPatient = (docId, patientData) => {
                state.firestoreDocId = docId;
                localStorage.setItem('myopiaTrackerDocId', docId);
                setupPatientListener(docId);
                welcomeName.textContent = patientData.name;
                showView(patientView);
                loadingOverlay.classList.add('hidden');
            }

            loginBtn.addEventListener('click', async () => {
                if (!isAuthReady) {
                    loginError.textContent = "正在连接，请稍候...";
                    loginError.classList.remove('hidden');
                    return;
                }
                const assignedId = assignedIdInput.value.trim();
                const name = loginNameInput.value.trim();

                if (!assignedId || !name) {
                    loginError.textContent = "ID和姓名均不能为空。";
                    loginError.classList.remove('hidden');
                    return;
                }

                if (assignedId.toUpperCase() === ADMIN_ID && name.toUpperCase() === ADMIN_ID) {
                    localStorage.setItem('myopiaTrackerDocId', ADMIN_ID); // For admin auto-login
                    setupResearcherListener();
                    showView(researcherView);
                    return;
                }
                
                loadingOverlay.classList.remove('hidden');
                loginError.classList.add('hidden');

                if (!registrationFields.classList.contains('hidden')) {
                    const age = patientAgeInput.value.trim();
                    const sex = patientSexInput.value;

                    if (age && sex) {
                        const newPatientData = { assignedId, name, age: parseInt(age), sex, totalCheckIns: 0, makeupChances: 3, lastCheckInDate: null, checkIns: {} };
                        const privateData = { checkIns: {}, makeupChances: 3 };
                        
                        const newDocRef = await addDoc(getParticipantsCollectionRef(), newPatientData);
                        await setDoc(getPrivateDataRef(newDocRef.id), privateData);

                        loginAsPatient(newDocRef.id, newPatientData);
                    } else {
                        loginError.textContent = "请填写所有注册信息。";
                        loginError.classList.remove('hidden');
                        loadingOverlay.classList.add('hidden');
                    }
                } else {
                    const q = query(getParticipantsCollectionRef(), where("assignedId", "==", assignedId), where("name", "==", name));
                    const querySnapshot = await getDocs(q);

                    if (querySnapshot.empty) {
                        loginError.textContent = "这是一个新的用户，请完成注册。";
                        loginError.classList.remove('hidden');
                        registrationFields.classList.remove('hidden');
                        loginBtn.textContent = "完成注册";
                        loadingOverlay.classList.add('hidden');
                    } else {
                        const userDoc = querySnapshot.docs[0];
                        loginAsPatient(userDoc.id, userDoc.data());
                    }
                }
            });

            // --- Data Logic ---
            const saveDataToFirestore = async () => {
                if (!state.firestoreDocId) return;
                const privateRef = getPrivateDataRef(state.firestoreDocId);
                const publicRef = getPublicSummaryRef(state.firestoreDocId);
                
                const totalCheckIns = Object.keys(state.checkIns).length;
                const lastCheckInDate = Object.keys(state.checkIns).sort().pop() || null;
                
                const currentPublicSnap = await getDoc(publicRef);
                const existingPublicData = currentPublicSnap.data() || {};

                const privateData = { checkIns: state.checkIns, makeupChances: state.makeupChances };
                const publicData = { ...existingPublicData, totalCheckIns, makeupChances: state.makeupChances, lastCheckInDate, checkIns: state.checkIns };
                
                try {
                    await setDoc(privateRef, privateData);
                    await setDoc(publicRef, publicData, { merge: true });
                } catch (error) { console.error("Error saving data: ", error); }
            };

            const setupPatientListener = (docId) => {
                if (state.unsubscribePatient) state.unsubscribePatient();
                state.unsubscribePatient = onSnapshot(getPrivateDataRef(docId), (docSnap) => {
                    if (docSnap.exists()) {
                        const data = docSnap.data();
                        state.checkIns = data.checkIns || {};
                        state.makeupChances = data.makeupChances !== undefined ? data.makeupChances : 3;
                    } else {
                         saveDataToFirestore();
                    }
                    renderCalendar();
                }, (error) => console.error("Patient listener error:", error));
            };

            const setupResearcherListener = () => {
                if (state.unsubscribeResearcher) state.unsubscribeResearcher();
                state.unsubscribeResearcher = onSnapshot(query(getParticipantsCollectionRef()), (querySnapshot) => {
                    participantsTableBody.innerHTML = '';
                    const participants = [];
                    querySnapshot.forEach((doc) => participants.push(doc.data()));
                    
                    participants.sort((a, b) => a.assignedId.localeCompare(b.assignedId)).forEach(p => {
                         const row = document.createElement('tr');
                        row.innerHTML = `
                            <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-700 font-medium">${p.assignedId}</td>
                            <td class="px-2 py-3 whitespace-nowrap text-sm text-gray-500">${p.name || 'N/A'}</td>
                            <td class="px-2 py-3 whitespace-nowrap text-sm font-medium text-gray-900">${p.totalCheckIns || 0}</td>
                            <td class="px-2 py-3 whitespace-nowrap text-sm font-medium text-red-500">${p.makeupChances !== undefined ? p.makeupChances : 'N/A'}</td>
                        `;
                        participantsTableBody.appendChild(row);
                    });
                }, (error) => {
                    console.error("Researcher listener error:", error);
                    const tableError = document.createElement('tr');
                    tableError.innerHTML = `<td colspan="4" class="text-center py-4 text-red-500">无法加载研究数据，请检查网络或权限。</td>`;
                    participantsTableBody.appendChild(tableError);
                });
            };

            exportCsvBtn.addEventListener('click', async () => {
                loadingOverlay.classList.remove('hidden');
                let csvContent = "data:text/csv;charset=utf-8,AssignedID,Name,Age,Sex,Date,Status\n";
                try {
                    const participantsSnap = await getDocs(getParticipantsCollectionRef());

                    for (const pDoc of participantsSnap.docs) {
                        const participant = pDoc.data();
                        const checkIns = participant.checkIns || {};
                        for (const date in checkIns) {
                            const name = `"${(participant.name || '').replace(/"/g, '""')}"`;
                            csvContent += `${participant.assignedId},${name},${participant.age},${participant.sex},${date},Checked\n`;
                        }
                    }
                    const encodedUri = encodeURI(csvContent);
                    const link = document.createElement("a");
                    link.setAttribute("href", encodedUri);
                    link.setAttribute("download", "myopia_checkin_data_all.csv");
                    document.body.appendChild(link);
                    link.click();
                    document.body.removeChild(link);
                } catch(e) {
                    console.error("CSV Export failed:", e);
                    alert("导出数据失败，可能是网络或权限问题。");
                } finally {
                    loadingOverlay.classList.add('hidden');
                }
            });

            const toISODateString = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate()).toISOString().split('T')[0];
            const updateStats = () => { totalCheckedDaysEl.textContent = Object.keys(state.checkIns).length; makeupChancesEl.textContent = state.makeupChances; };
            const showMakeupModal = (date) => { state.dateToMakeup = date; makeupDateText.textContent = date; makeupModal.classList.remove('hidden'); };
            const hideMakeupModal = () => { state.dateToMakeup = null; makeupModal.classList.add('hidden'); };
            const renderCalendar = () => {
                calendarBody.innerHTML = ''; const year = state.currentDate.getFullYear(); const month = state.currentDate.getMonth(); currentMonthYear.textContent = `${year}年 ${month + 1}月`; const firstDayOfMonth = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const todayISO = toISODateString(state.today); for (let i = 0; i < firstDayOfMonth; i++) calendarBody.appendChild(document.createElement('div')); for (let i = 1; i <= daysInMonth; i++) { const cell = document.createElement('div'); cell.className = 'day-cell w-full aspect-square flex items-center justify-center rounded-full text-sm font-semibold'; const dayDate = new Date(year, month, i); const dayISO = toISODateString(dayDate); cell.textContent = i; cell.dataset.date = dayISO; if (state.checkIns[dayISO]) { cell.classList.add('checked'); cell.innerHTML = `${i}<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" /></svg>`; } else if (dayISO === todayISO) { cell.classList.add('today', 'cursor-pointer'); cell.addEventListener('click', () => handleCheckIn(dayISO)); } else if (dayDate < state.today) { cell.classList.add('missed'); if (state.makeupChances > 0) { cell.classList.add('can-makeup'); cell.title = "点击补卡"; cell.addEventListener('click', () => handleMakeup(dayISO)); } } else { cell.classList.add('future'); } calendarBody.appendChild(cell); } updateStats();
            };
            const handleCheckIn = (date) => { if (state.checkIns[date]) return; state.checkIns[date] = true; saveDataToFirestore(); };
            const handleMakeup = (date) => { if (state.makeupChances <= 0 || state.checkIns[date]) return; showMakeupModal(date); };
            prevMonthBtn.addEventListener('click', () => { state.currentDate.setMonth(state.currentDate.getMonth() - 1); renderCalendar(); });
            nextMonthBtn.addEventListener('click', () => { state.currentDate.setMonth(state.currentDate.getMonth() + 1); renderCalendar(); });
            cancelMakeupBtn.addEventListener('click', hideMakeupModal);
            confirmMakeupBtn.addEventListener('click', () => { if (state.dateToMakeup && state.makeupChances > 0) { state.checkIns[state.dateToMakeup] = true; state.makeupChances--; saveDataToFirestore(); hideMakeupModal(); } });
        });
    </script>
</body>
</html>

